export type ConfigDictValue = boolean | number | string | null | ConfigDictArray | ConfigDict
export interface ConfigDict {
  [key: string]: ConfigDictValue
}
export interface ConfigDictArray extends Array<ConfigDictValue> {}

/**
 * Type to represent the class-type of Serializable objects.
 *
 * Ie the class prototype with access to the constructor and any
 * static members/methods.  Instance methods are not listed here.
 *
 * Source for this idea: https://stackoverflow.com/a/43607255
 */
export type SerializableConstructor<T extends Serializable> = {
  // tslint:disable-next-line:no-any
  new (...args: any[]): T
  className: string
  fromConfig: FromConfigMethod<T>
}
export type FromConfigMethod<T extends Serializable> = (
  cls: SerializableConstructor<T>,
  config: ConfigDict
) => T

/**
 * Serializable defines the serialization contract.
 *
 * TFJS requires serializable classes to return their className when asked
 * to avoid issues with minification.
 */
export abstract class Serializable {
  /**
   * Return the class name for this class to use in serialization contexts.
   *
   * Generally speaking this will be the same thing that constructor.name
   * would have returned.  However, the class name needs to be robust
   * against minification for serialization/deserialazation to work properly.
   *
   * There's also places such as initializers.VarianceScaling, where
   * implementation details between different languages led to different
   * class hierarchies and a non-leaf node is used for serialization purposes.
   */
  getClassName(): string {
    return (this.constructor as SerializableConstructor<Serializable>).className
  }

  /**
   * Return all the non-weight state needed to serialize this object.
   */
  abstract getConfig(): ConfigDict

  /**
   * Creates an instance of T from a ConfigDict.
   *
   * This works for most descendants of serializable.  A few need to
   * provide special handling.
   * @param cls A Constructor for the class to instantiate.
   * @param config The Configuration for the object.
   */
  static fromConfig<T extends Serializable>(
    cls: SerializableConstructor<T>,
    config: ConfigDict
  ): T {
    return new cls(config)
  }
}

/**
 * Maps string keys to class constructors.
 *
 * Used during (de)serialization from the cross-language JSON format, which
 * requires the class name in the serialization format matches the class
 * names as used in Python, should it exist.
 */
export class SerializationMap {
  private static instance: SerializationMap
  classNameMap: {
    [className: string]: [SerializableConstructor<Serializable>, FromConfigMethod<Serializable>]
  }

  private constructor() {
    this.classNameMap = {}
  }

  /**
   * Returns the singleton instance of the map.
   */
  static getMap(): SerializationMap {
    if (SerializationMap.instance == null) {
      SerializationMap.instance = new SerializationMap()
    }
    return SerializationMap.instance
  }

  /**
   * Registers the class as serializable.
   */
  static register<T extends Serializable>(cls: SerializableConstructor<T>) {
    SerializationMap.getMap().classNameMap[cls.className] = [cls, cls.fromConfig]
  }
}
